#
# - Removed tweets to user function. !log <twitter> <text> now tweets to @indiatwit, otherwise just clips 	(11/5/08)
# - Improved mapping (check individual functions for explanation)										  	(13/5/08)

# 		* Nicknames are checked for existing mappings of their root	
#		  before adding to mapping list to reduce redundancy
#		* Channel is regularly queried to keep mappings up-to-date
#		* Mapping info can be retreived

#			; to get twitter handle mapped to an irc nickname
#			; to generate twitter user URL from irc nickname
# 
# - Logs and provides chatlogs via !review x where x = number of lines to retrieve							(12/05/08)
#

import irclib
import re
import time
import simplejson
import twitter
import pastebin

irclib.DEBUG = True

# Connection information
network = 'irc.freenode.net'
port = 6667
channel = ''
nick = ''
pass_ = ""
name = ''
path = ""
to = twitter.Api('', '')
totest = twitter.Api('','')
(authed, maps) = ({}, {})
last_update = time.localtime()

def pubMSG(connection, event):				# main event handler for most of the commannds
	ev = event.arguments()[0];
	fr = event.source().split('!')[0] 		# to separate nick from the entire address
	
	if ev.startswith('!'):
		m = re.match('^!(\w+)[ ]?(.*)', ev)
		try:								# try catch block, return if syntax is wrong
			func = m.group(1)
			arg = m.group(2)
		except:
			return
		
		if (func == "addo") & (arg != ""): 	# to add to ops
			add_to_ops(fr, arg)
			return
			
		if (func == "addv") & (arg != ""): 	# to add to voice
			add_to_voice(fr, arg)
			return
		
		if (func == 'op') | (func == 'v'):	# self op and voice
			selfACL(fr, func)
			return
		
		if (func == 'log') & (arg != ""): 	# to clip
			log(fr, arg)
			return
		
		if (func == 'review'):				# generate a pastebin of last 'x' lines
			if arg:
				review(fr, arg);
			else:
				review(fr, 50)
			return
		
		if (func == 'gl') & (arg != ""): 	# to retrieve clips
			getlog(arg)
			return
		
		if (func == 'topic') & (arg != ""): # set the topic
			topic(fr, arg)
			return
		
		if (func == 'map') & (arg != ""):	# map irc nicknames to twitter handles
			ret = map(fr, arg)
			if ret == 1:
				msgServer( "[Nicknames mapped]")
			elif ret == 0:
				msgServer( "[Only Ops can map]")
			else:
				msgServer( "[Nicknames couldn't be mapped]")
			return
		
		if (func == 'gm') & (arg != ""):	# get mapping data
			getMaps(arg, 1)
			return
		
		if (func == 'turl') & (arg != ""):	# generate twitter url from irc nickname
			turl(arg)
			return;
		
		if func == 'dc':					# disconnect from server
			if file(path + '\ops.log').read().find(fr) != -1:
				server.disconnect()
				exit()
		
	f = open(path + "\chanlog.log", 'a') 	# log everything
	f.write(str(fr + ":" + ev + "\n"))
	f.close()

def add_to_ops(fr, nick):
	p = path + '\ops.log'
	f = open(p, 'a')

	if file(path + "\ops.log").read().lower().find(fr.lower()) != -1:	# initiator is an op
		if file(path + "\ops.log").read().lower().find(nick.lower()) != -1:	# doesn't already exist
			msgServer( "[" + nick + " already in list]" )
			return

		ops = " " + nick # space separated list
		f.write(ops)
		f.close()
		server.mode(channel, "o " + nick) 	# op the user
		msgServer( "[" + nick + " was added to Ops list]" )
	else:
		msgServer( "[Only operators can add to list]" )

def add_to_voice(fr, nick): 				# same as above
	p = path + '\ov.log'
	f = open(p, 'a')

	if file(path + '\ops.log').read().lower().find(fr.lower()) != -1: # initiator is an op
		if file(p).read().lower().find(nick.lower()) != -1:
			msgServer( "[" + nick + " already in list]" )
			return

		ops = " " + nick
		f.write(ops)
		f.close()
		server.mode(channel, "v " + nick)
		msgServer( "[" + nick + " was added to Voice list]" )
	else:
		msgServer( "[Only operators can add to list]" )

def log(nick, text):
	curr_time = time.strftime('%d-%m-%y') 	# current date for private log
	p = path + '\clips.log'
	flag = 0
	
	tw = re.match("(twitter|debug)?[ ]?(.*)", text)	# analyze 'text' for twitter|debug switch
	to_tweet = tw.group(2).lstrip()
	
	if tw.group(1) == "twitter":			# if tweeting to @indiatwits
		if haveAccess(nick):
			to_st = to.PostUpdate( urlenc( mapIt(to_tweet, 1)[0:140] ))
			flag = 1
		else:
			msgServer("[Sorry, only operators and voiced users can tweet]")
	elif tw.group(1) == "debug":			# if tweeting to debug
		to_st = totest.PostUpdate( urlenc( mapIt(to_tweet, 1)[0:140] ))
		msgServer("[Tweeted to Debug]")
		return
	
	f = open(p, 'a')
	f.write(to_tweet + ' [' + str(curr_time) + ' / ' + nick + ']' + "\n")	# write to file
	f.close()
		
	if flag == 1:
		msgServer( "[Clip saved and tweeted at " + str(curr_time) + "]")
	else:
		msgServer( "[Clip saved at " + str(curr_time) + "]")

def getlog(key):							# get clips
	p = path + '\clips.log'
	f = open(p, 'r')
	flag = 1
	c = 0
	m = 0
	
	msgServer( "-----------------------")
	while True:
		line = f.readline()
		if line == '':
			break
		
		if line.lower().find(key.lower()) != -1:	# if key found anywhere in line (case insensitive)
			if c <= 7:						# only show 7
				msgServer( line)
				c = c + 1
				time.sleep(1)				# 1 sec gap between server posts
			else:
				c = c + 1
				flag = 1
	# end of while
	
	if c >= 7:								# only show last 7
		m = 7
	else:
		m = c
	
	if flag == 0:
		msgServer( "-- Sorry, no logs found --")
	else:
		msgServer( "--- " + str(m) +" / " + str(c) + " log(s) shown. ---")

def topic(fr, text): 						# set the topic
	if file(path + '\ops.log').read().lower().find(fr.lower()) != -1:
		status = to.PostUpdate("Discussing: " + text + ". Join us http://is.gd/dRD")	# post to @indiatwits
		topic = "Welcome to IndiaTwits! The Hangout of the Indian Twitter Community :) Current Topic: "+ text +" // Learn More about IndiaTwits at <http://is.gd/dRD> Join the IndiaTwits Folding@Home Team. More at <http://lin.cr/nq>. Follow @indiatwits on Twitter to get regular updates. Enjoy your stay!"
		server.topic(channel, topic)
	else:
		server.privmsg (channel, "[Sorry, Ops only]")

def auto_works(connection, event):			# auto auth on new user join
	fr = event.source().split('!')[0]
	if file(path + '\ops.log').read().lower().find(fr.lower()) != -1:
		server.mode(channel, 'o ' + fr)
		return
	
	if file(path + '\ov.log').read().lower().find(fr.lower()) != -1:
		server.mode(channel, 'v ' + fr)
		return

def selfACL(nick, lvl):						# to self auth
	if lvl == 'op':
		if file(path + '\ops.log').read().lower().find(nick.lower()) != -1:
			server.mode(channel, 'o ' + nick)
		else:
			msgServer('[Not an operator]')
		return
	if lvl == 'v':		
		if file(path + '\ov.log').read().lower().find(nick.lower()) != -1:
			server.mode(channel, 'v ' + nick)
		else:
			msgServer('[Not voiced]')
		return

def review(n, c):							# get chat logs
	try:									# try error check to convert string to closest integer
		c = int(round(float(c), 0))
	except:
		msgServer("[Invalid line count]")
		return
	if c > 300:								# upper limit is 300 lines
		msgServer("[Sorry, upper limit is 300]")
		return
	
	msgServer("[Hang on, this might take a minute]")
	lines = file('c:\python25\code\chanlog.log').read().split('\n')
	cumulate = ""
	for line in lines[len(lines)-c:len(lines)]:
		parts = line.split(':')
		nick = parts[0]
		text = parts[1:len(parts)]
		cumulate += str(nick + ": " + "".join(text) + "\n")
		
	res = pastebin.paste(urlenc(cumulate), "#indiatwits chat log", nick, "10 minutes")	# pastebin work
	url = pastebin.got_page(res)
	msgServer("@" + n + ': The last ' + str(c) + ' lines: ' + str(url))

def authNicks(connection, event):			# auto auth nicks at regular intervals
	nicks = event.arguments()[2]
	mapFileUpd(nicks);						# update mappings as well
	for i in nicks.split(' '):
		if file(path + '\ops.log').read().lower().find(i.lower()) != -1:
			server.mode(channel, 'o ' + i)
			continue
		
		if file(path + '\ov.log').read().lower().find(i.lower()) != -1:
			server.mode(channel, 'v ' + i)
			continue

#
# All mapping functions
#

def map(nick, arg):							# map irc nickname to twitter handle
	if haveAccess(nick) == 0:
		return 0
	nicks = arg.split(' ')
	log_text = nicks[0] + ":" + nicks[1] + "\n"	# only take first two nicknames specified
	f = open(path + '\maps.log', 'a')
	f.write(log_text)
	f.close()
	return 1

def mapUpd(connection, event):				# update mapping array at regular intervals
	nicks = file(path + "\maps.log").read().split("\n")
	for line in nicks:
		if line.find(":") != -1:
			line = line.split(":")
			maps.update({line[0]:line[1]})
	server.names_(channel)					# make call to NAMREPLY

def mapFileUpd(nicks):						# keep mapping file/list updated
	mapped = file(path + "\maps.log").read().split("\n")
	new = ""
	for n in nicks.split(" "):				# nicks is a space separated list of irc nicknames
		if n == "":
			break
		n = n.replace("@", "").replace("+", "")	# remove IRC symbols
		if (n.lower() == 'itbot') | (n.lower() == 'chanserv'): # remove bots
			continue
		flag = 0
		for m in mapped:
			temp = m.split(":")				# split into key:val pair
			if temp[0] == n:				# if n exists in mapping
				flag = 0
				break						# skip this iteration
			else:
				flag = 1					# else update the mapping
		if flag == 1:
			#
			# This block checks if the incoming nickname has
			# already been mapped to a handle. For example,
			# 'adityavm^away' & 'adityavm^sleep' have a root 
			# 'adityavm', which is mapped to 'aditya'. Hence
			# the derivatives should be mapped to the root nick
			# map as well.
			#
			stripped = getRootNick(n)		# find existing mapping for root nick
			rootnick = stripped
			for i in mapped:
				temp = i.split(":")
				if temp[0] == stripped:		# if rootnick mapped
					rootnick = temp[1]		# set mapping of incoming nick to existing map
					break
			new += n + ":" + rootnick + "\n"
	cumulate = str(new + file(path + "\maps.log").read()).lstrip().rstrip()	# put new mappings at the top so that mistakes are removed by collisions
	f = open(path + "\maps.log", 'w')
	f.write(cumulate)
	f.close()

def mapIt(text, at):						# map irc nicks to twitter handles
	text = text.replace("@","").replace("+","").replace("{","{").replace("}","") # remove all special chars
	if at:									# if prefix handles with @
		rep = "@"
	else:
		rep = ""
	
	(done,cumus) = ([],{})					# to store handles already mapped
	for m in maps:
		if done.count(maps[m]) >= 1:		# if handle already mapped, skip
			continue
		cumu = ""
		r = re.search("("+ maps[m].replace("[","\[") +")([|\^])?(\w+)?", text)
		if r != None:						# accumulate entire nickname to be mapped
			for i in range(1,4):
				if r.group(i) != None:
					cumu += r.group(i)
		if cumu != "":
			cumus.update({cumu:maps[m]})	# add nick to array of to-be-mapped
			done.append(maps[m])			# add to nicks mapped

	for i in cumus:
		text = text.replace(i, rep+cumus[i])	# do replacement for nicks
	
	for m in maps:							# to map missed names
		if done.count(maps[m]) >= 1:		# if not already done
			continue
		text = text.replace(m, rep+maps[m])
	if text.startswith('@'):				# remove '@' to prevent a reply
		text = text[1:len(text)]
	return text

def getMaps(arg, echo):						# get existing mapping details
	f = file(path + "\maps.log").read().split("\n")
	for m in f:
		n = m.split(":")
		if n[0] == getRootNick(arg):		# if mapping for rootnick from arg
			if echo:
				msgServer("[\"" + arg +"\" is @" + n[1] + "]")
			return n[1]
	if echo:
		msgServer("[Sorry, no mapping found]")
	return 0

 def turl(arg):								# generate twitter url from nickname
	nick = getMaps(arg, 0)					# get mapped nick (with no echo)
	if not nick:
		msgServer("[No mapping found]")
	else:
		msgServer("[\"" + arg +"\"'s Twitter URL: http://twitter.com/" + nick + "]")

# Other functions
def urlenc(str):							# url encoding for pastebin
	return str.replace('<', '&lt;').replace('>', '&gt;')

def getRootNick(nick):						# try to get rootnick from derivatives
	return re.search("([^\W_|\/'`!^]+)", nick).group(1)

def haveAccess(nick):						# if passed nick has op/v access
	access = (file(path + '\ops.log').read().lower().find(nick.lower()) != -1) | (file(path + '\ov.log').read().lower().find(nick.lower()) != -1) # only for ops and voice
	if access:
		return 1
	else:
		return 0

def msgServer(text):						# shortcut to send msg to server
	server.privmsg( channel, text )

def chanlogac(connection, event): 			# to log actions (must be done separately)
	if event.arguments()[0] != "ACTION":
		return;
	ev = event.arguments()[1];
	fr = event.source().split('!')[0]
	
	f = open(path + "\chanlog.log", 'a')
	f.write(str(" ** " + fr + ev + "\n"))
	f.close()

# Create an IRC object
irc = irclib.IRC()

# Register handlers (should be before server connections)
irc.add_global_handler('pubmsg', pubMSG)
irc.add_global_handler('ctcp', chanlogac)
irc.add_global_handler('join', auto_works)
irc.add_global_handler('ping', mapUpd)
irc.add_global_handler('namreply', authNicks)

# Create a server object, connect and join the channel
server = irc.server()
server.connect ( network, port, nick, pass_, ircname = name )
server.join ( channel )
mapUpd('irc.freenode.org','#indiatwits')

# Jump into an infinite loop
irc.process_forever()